package ar.com.spsolutions.splibrary.mappings;

import java.io.File;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import junit.framework.Assert;

import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.EntityPersister;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;

import ar.com.spsolutions.splibrary.entity.Entity;
import ar.com.spsolutions.splibrary.utils.EntitySubclassFinder;
import ar.com.spsolutions.splibrary.utils.GenericTestRepository;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
		"classpath*:/META-INF/spring-persistence-context.test.xml",
		"classpath*:/META-INF/spring-datasource-context.test.xml" })
@TransactionConfiguration(transactionManager = "persistence.transactionManager")
public class AutomagicallyMappingTestCase {

	private static final String RAWTYPES = "rawtypes";
	@Autowired
	private GenericTestRepository repository;
	@Autowired
	private SessionFactory sessionFactory;

	private Map<Class<?>, Object> instances = new HashMap<Class<?>, Object>();

	@Test
	@Transactional
	@SuppressWarnings(AutomagicallyMappingTestCase.RAWTYPES)
	/**
	 * Test que chequea que todas las subclases de Entity esten mapeadas
	 */
	public void checkAllEntitiesIsMappeds() {
		final List<Class<?>> entities = this.findEntitySubclasses();
		this.getSessionFactory().openSession();
		final Map<String, ClassMetadata> metadata = this.getSessionFactory()
				.getAllClassMetadata();
		Assert.assertEquals("No todas las entidades estan mapeadas",
				entities.size(), metadata.size());
		final Iterator iterator = this.getIterator();
		while (iterator.hasNext()) {
			final String entityClassName = this.getEntityName(iterator);
			try {
				final Class<?> target = Class.forName(entityClassName);
				if (!entities.remove(target)) {
					Assert.fail("class " + target + " is not a Entity");
				}
			} catch (final ClassNotFoundException e) {
				Assert.fail("Mapped class " + entityClassName + " is not found");
			}
		}
		if (!entities.isEmpty()) {
			Assert.fail("Class " + entities.get(0).getCanonicalName()
					+ " is not mapped");
		}
	}

	@SuppressWarnings(AutomagicallyMappingTestCase.RAWTYPES)
	@Test
	@Transactional
	/**
	 * Chequea que los mappins esten correctos
	 */
	public void checkMapping() {
		this.getSessionFactory().openSession();
		final Iterator iterator = this.getIterator();
		while (iterator.hasNext()) {
			final String entityClassName = this.getEntityName(iterator);
			try {
				if (this.isTestable(entityClassName)) {
					final Class<?> builderClass = this
							.getBuilderClass(entityClassName);
					final Method instanceCreatorMethod = this
							.getInstanceCreationMethod(builderClass);
					final Object instance = this.getInstance(builderClass,
							instanceCreatorMethod);
					this.checkMapping((Entity) instance);
				}
			} catch (final ClassNotFoundException e) {
				Assert.fail("Builder for class " + entityClassName
						+ " not found");
			} catch (final SecurityException e) {
				Assert.fail("Builder for class " + entityClassName
						+ " Security problem");
			} catch (final NoSuchMethodException e) {
				Assert.fail("method " + entityClassName
						+ "Builder#createInstanceForTest() not found");
			} catch (final IllegalArgumentException e) {
				Assert.fail("method called"
						+ entityClassName
						+ "Builder#createInstanceForTest() has argument problem :s");
			} catch (final IllegalAccessException e) {
				Assert.fail("method "
						+ entityClassName
						+ "Builder#createInstanceForTest() has access problem :s");
			} catch (final InvocationTargetException e) {
				Assert.fail("method "
						+ entityClassName
						+ "Builder#createInstanceForTest() InvocationTargetException");
			}
		}
	}

	public List<Class<?>> findEntitySubclasses() {
		final ClassLoader systemClassLoader = ClassLoader
				.getSystemClassLoader();
		final URL rootURL = systemClassLoader.getResource(".");
		final String targetPath = this.getTargetPath(rootURL);
		try {
			final URLClassLoader classLoader = new URLClassLoader(
					new URL[] { new File(targetPath).toURI().toURL() });
			final File rootFolder = new File(targetPath);
			final List<Class<?>> collector = new ArrayList<Class<?>>();
			new EntitySubclassFinder().find(classLoader, rootFolder, collector,
					"");
			return collector;
		} catch (final MalformedURLException e) {
			Assert.fail("Cannot load classloader");
			return new ArrayList<Class<?>>();
		}

	}

	private String getTargetPath(final URL rootURL) {
		// hackso!!!!
		return rootURL.getPath().replaceAll("\\/test-classes\\/", "")
				.replaceAll("persistence", "model")
				+ "/classes/";
	}

	private boolean isTestable(final String entityClassName) {
		try {
			final Class<?> entityClass = Class.forName(entityClassName);
			final boolean isNotAbstract = (entityClass.getModifiers() & Modifier.ABSTRACT) != Modifier.ABSTRACT;
			final boolean isNotInterface = !entityClass.isInterface();
			return isNotAbstract & isNotInterface;
		} catch (final ClassNotFoundException e) {
			return false;
		}
	}

	private Object getInstance(final Class<?> builderClass,
			final Method instanceCreatorMethod) throws IllegalAccessException,
			InvocationTargetException {
		final Object instance = instanceCreatorMethod.invoke(builderClass,
				new Object[0]);
		return instance;
	}

	private Method getInstanceCreationMethod(final Class<?> builderClass)
			throws NoSuchMethodException {
		final Method instanceCreatorMethod = builderClass.getDeclaredMethod(
				"createInstanceForTest", new Class[0]);
		return instanceCreatorMethod;
	}

	private Class<?> getBuilderClass(final String entityClassName)
			throws ClassNotFoundException {
		final Class<?> builderClass = Class
				.forName(entityClassName + "Builder");
		return builderClass;
	}

	@SuppressWarnings("rawtypes")
	private String getEntityName(final Iterator iterator) {
		final EntityPersister entityPersister = (EntityPersister) iterator
				.next();
		return entityPersister.getEntityName();

	}

	@SuppressWarnings("rawtypes")
	private Iterator getIterator() {
		final Map metadata = this.getSessionFactory().getAllClassMetadata();
		return metadata.values().iterator();

	}

	public GenericTestRepository getRepository() {
		return this.repository;
	}

	public void setRepository(final GenericTestRepository repository) {
		this.repository = repository;
	}

	@Transactional
	public void save(final Entity entity) {
		this.getHibernateTemplate().save(entity);
		this.getHibernateTemplate().flush();
	}

	private HibernateTemplate getHibernateTemplate() {
		return this.getRepository().getHibernateTemplate();
	}

	@Transactional
	public Entity findById(final Class<?> entityClass, final Serializable id) {
		return (Entity) this.getHibernateTemplate().get(entityClass, id);
	}

	public void checkMapping(final Entity target) {
		this.save(target);
		final Entity retrived = this
				.findById(target.getClass(), target.getId());
		Assert.assertEquals("Error testeando mappings de la clase "
				+ target.getClass().getCanonicalName(), target, retrived);
	}

	public SessionFactory getSessionFactory() {
		return this.sessionFactory;
	}

	public void setSessionFactory(final SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	public Map<Class<?>, Object> getInstances() {
		return this.instances;
	}

	public void setInstances(final Map<Class<?>, Object> instances) {
		this.instances = instances;
	}

}
